﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Phoenix.ReflectionExtensions;
using PServiceBus.Core.Runtime.Extensions;
using PServiceBus.Core.Provider;
using System.Collections;
using System.ComponentModel;
using PServiceBus.Services.Gateway.Extensions;
using PServiceBus.Core.Runtime.Attributes;
using System.Linq.Expressions;
using PServiceBus.Core.Runtime;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using PServiceBus.Gateway.Interfaces.Exceptions;

namespace PServiceBus.Services.Gateway.Runtime {
    /// <summary>
    /// This class is for publishing/registering topic
    /// </summary>
    public sealed class Topic {
        private string _name;
        private Guid _id;
        private FilterExpression _filter;
        private string _description = string.Empty;
        private TimeSpan? _expiresIn;
        internal Dictionary<string, string> _headers = new Dictionary<string, string>();
        private Dictionary<string, string> _contractDict = new Dictionary<string, string>();
        internal Dictionary<string, object> _publishDict = new Dictionary<string, object>();
        private List<Dictionary<string, object>> _publishDicts = new List<Dictionary<string, object>>();
        static internal readonly Type _descriptionAttributeType = typeof(DescriptionAttribute);
        static internal readonly Type _topicHeaderAttributeType = typeof(TopicHeaderAttribute);
        private static readonly ConcurrentDictionary<string, Topic> _topics = new ConcurrentDictionary<string, Topic>();
        
        private Topic(string name) {
            _name = name;
            IsValid = true;
            CaseSensitive = true;
        }
        public string TopicDescription { get { return _description; } }
        internal bool NeedMessageHeaders { get; set; }
        internal bool CaseSensitive { get; set; }
        public Dictionary<string, string> ContractDict { get { return _contractDict; } internal set { _contractDict = value; } }
        private Dictionary<string, object> PublishDict { get { return _publishDict; } }
        public List<Dictionary<string, object>> PublishDicts { get { return _publishDicts; } internal set { _publishDicts = value; } }
        public string TopicName { get { return _name; } }
        public Guid TopicID { get { return _id; } }
        public string TopicFilter {
            get {
                return _filter != null ? _filter.ToString() : string.Empty;
            } 
        }
        internal TimeSpan? ExpiresIn { get { return _expiresIn; } set { _expiresIn = value; } }
        internal Dictionary<string, string> Headers { get { return _headers; } set { _headers = value; } }
        /// <summary>
        /// Set current topic's id
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        internal Topic Id(Guid id) {
            _id = id;
            return this;
        }
        /// <summary>
        /// Return an instance of the specific topic name
        /// If topic does not exist, it will return an instance of the topic in an invalid state
        /// Invalid topic which invoke any method will throw an exception
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        /// <exception cref="TopicNotRegisteredException">TopicNotRegisteredException</exception>
        public static Topic Select(string name) {
            var topic = default(Topic);
            if (_topics.TryGetValue(name, out topic)) {
                topic = topic.Clone();
            } else {
                ESB.Execute(messageBus => topic = messageBus.SelectTopic(name).ToTopic());
                if (topic != null && topic.IsValid) _topics[name] = topic;
            }
            return topic == null ? Topic.New(name).Valid(false) : topic;
        }

        /// <summary>
        /// Select Topic and apply filter
        /// </summary>
        /// <typeparam name="TTopic"></typeparam>
        /// <param name="expression"></param>
        /// <returns></returns>
        public static Topic SelectWithFilter<TTopic>(Expression<Func<TTopic, bool>> expression) where TTopic : class {
            return Topic.Select<TTopic>().FilterWith(expression);
        }

        /// <summary>
        /// Returns true if topic exists
        /// </summary>
        /// <typeparam name="TTopic"></typeparam>
        /// <returns></returns>
        public static bool Exists<TTopic>() where TTopic : class {
            var type = typeof(TTopic);
            return Exists(type);
        }

        public static bool Exists(Type type) {
            return Topic.Select(type.Name).IsValid;
        }

        public static bool Exists(string name) {
            return Topic.Select(name).IsValid;
        }

        public static void RegisterIfNotExist(string name) {
            if (!Topic.Exists(name)) Topic.Register(name);
        }

        public static void RegisterIfNotExist<TTopic>() where TTopic : class {
            if (!Topic.Exists<TTopic>()) Topic.Register<TTopic>();
        }

        /// <summary>
        /// Return the current state of the topic instance
        /// </summary>
        public bool IsValid { get; private set; }
        /// <summary>
        /// Create a new topic with the specified name
        /// </summary>
        /// <param name="name"></param>
        /// <returns></returns>
        public static Topic New(string name) {
            return new Topic(name);
        }
        /// <summary>
        /// Set a filter on the topic when subscribing to it
        /// </summary>
        /// <param name="expression"></param>
        /// <returns></returns>
        public Topic Filter(string expression) {
            return Filter(FilterExpression.Raw(expression));
        }

        /// <summary>
        /// Set a filter on the topic when subscribing to it
        /// </summary>
        /// <param name="filter"></param>
        /// <returns></returns>
        public Topic Filter(FilterExpression filter) {
            _filter = filter;
            return this;
        }


        /// <summary>
        /// Add contract information to the current topic
        /// </summary>
        /// <param name="name">Name of field that will be in the contract</param>
        /// <param name="description">Description of the field that will be in the contract</param>
        /// <returns></returns>
        public Topic AddParameter(string name, string description) {
            ValidateState();
            _contractDict[name] = description;
            return this;
        }
        /// <summary>
        /// Set the contract fields that will be published
        /// </summary>
        /// <param name="name">Name of field that will be published</param>
        /// <param name="value">Value of the field to be published</param>
        /// <returns></returns>
        public Topic SetParameter(string name, object value) {
            ValidateState();
            _publishDict[name] = value.Translate();
            return this;
        }

        public Topic ResequenceWith(string groupID, int sequenceID) {
            SetHeader(Constants.ESB_GROUP_ID_KEY, groupID);
            SetHeader(Constants.ESB_SEQUENCE_ID_KEY, sequenceID.ToString());
            return this;
        }

        public Topic NeedHeaders(bool value) {
            NeedMessageHeaders = value;
            return this;
        }

        public Topic CaseSensitiveFilter(bool value) {
            CaseSensitive = value;
            return this;
        }

        /// <summary>
        /// Register the topic
        /// </summary>
        public void Register() {
            ValidateState();
            ESB.Execute(messageBus => messageBus.RegisterTopic(this.ToTopicInfo()));
        }


        /// <summary>
        /// Register topic based on specified type properties.
        /// Using type name as topic name
        /// Using properties as contract information
        /// Using DescriptionAttribute for contract descriptions
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public static void Register<TTopic>() where TTopic : class {
            var type = typeof(TTopic);
            Register(type);
        }

        public static void Register(string name) {
            Topic.New(name).Register();
        }

        /// <summary>
        /// Register topic based on specified type properties.
        /// Using type name as topic name
        /// Using properties as contract information
        /// Using DescriptionAttribute for contract descriptions
        /// </summary>
        /// <param name="type"></param>
        public static void Register(Type type) {
            var topic = Topic.New(type.Name);
            var typeAttributes = type.GetCustomAttributes(_descriptionAttributeType, true);
            var topicDesc = typeAttributes != null && typeAttributes.Length > 0 ?
                (typeAttributes[0] as DescriptionAttribute).Description : string.Empty;
            var props = type.GetPropertiesEx();
            topic.Description(topicDesc);
            foreach (var prop in props) {
                if (!(prop.CanWrite && prop.CanRead)) continue;
                var propDescAttribute = prop.GetCustomAttributes(_descriptionAttributeType, true);
                var propTopicHeaderAttribute = prop.GetCustomAttributes(_topicHeaderAttributeType, true);
                if (propTopicHeaderAttribute != null && propTopicHeaderAttribute.Length > 0) continue;
                var propDesc = propDescAttribute != null && propDescAttribute.Length > 0 ?
                    (propDescAttribute[0] as DescriptionAttribute).Description : string.Empty;
                topic.AddParameter(prop.Name, propDesc);
            }
            topic.Register();
        }

        /// <summary>
        /// Publish specified message to topic by using message type as topic name
        /// </summary>
        /// <typeparam name="TTopicMessage"></typeparam>
        /// <param name="topicMessage"></param>
        public static void PublishMessage<TTopicMessage>(TTopicMessage topicMessage) where TTopicMessage : class {
            var type = topicMessage.GetType();
            var topic = Topic.Select(type.Name);
            topic.Publish(topicMessage);
        }

        /// <summary>
        /// Published collection of message to topic by using message type as topic name
        /// </summary>
        /// <typeparam name="TTopicMessage"></typeparam>
        /// <param name="topicMessages"></param>
        public static void PublishMessages<TTopicMessage>(ICollection<TTopicMessage> topicMessages) where TTopicMessage : class {
            if (!topicMessages.Any()) return;
            var type = topicMessages.First().GetType();
            var topic = Topic.Select(type.Name);
            topic.PublishMany(topicMessages);
        }


        /// <summary>
        /// Return an instance of Topic based on the specified type information[name]
        /// </summary>
        /// <typeparam name="TTopic"></typeparam>
        /// <returns></returns>
        public static Topic Select<TTopic>() where TTopic : class {
            var type = typeof(TTopic);
            return Topic.Select(type.Name);
        }

        /// <summary>
        /// Add Header information for message before publishing it
        /// Currently used internally
        /// </summary>
        /// <param name="key"></param>
        /// <param name="value"></param>
        /// <returns></returns>
        public Topic SetHeader(string key, string value) {
            ValidateState();
            _headers[key] = value;
            return this;
        }


        /// <summary>
        /// Set the description of topic for registration
        /// </summary>
        /// <param name="description"></param>
        /// <returns></returns>
        public Topic Description(string description) {
            _description = description;
            return this;
        }

        public Topic FilterWith<TMessage>(Expression<Func<TMessage, bool>> expression) where TMessage : class {
            var filter = ExpressionGenerator.ExpressionToFilter(expression);
            return Filter(filter);
        }
        
        /// <summary>
        /// Set the expiration for the message that will be published when calling publish method
        /// </summary>
        /// <param name="expiresIn"></param>
        /// <returns></returns>
        public Topic SetMessageExpiration(TimeSpan expiresIn) {
            ValidateState();
            _expiresIn = expiresIn;
            return this;
        }

        public Topic SetMessageExpiration(long expiresIn) {
            if (expiresIn > 0) SetMessageExpiration(TimeSpan.FromMilliseconds(expiresIn));
            return this;
        }

        /// <summary>
        /// Add fields to be published using the specified POCO object to the current underlying dictionary[key,value]
        /// </summary>
        /// <typeparam name="TMessage"></typeparam>
        /// <param name="message"></param>
        public void Message<TMessage>(TMessage message) {
            ValidateState();
            _publishDict = _publishDict.MergeWith(message.ToDict());
        }


        /// <summary>
        /// Publish message using the specified POCO object
        /// </summary>
        /// <typeparam name="TMessage"></typeparam>
        /// <param name="message"></param>
        public void Publish<TMessage>(TMessage message) {
            ValidateState();
            var type = message.GetType();
            if (typeof(ICollection).IsAssignableFrom(type)) throw new ESBException("Collection of messages cannot be published using this method. Refer to PublishMany method");
            if (typeof(IDictionary).IsAssignableFrom(type)) throw new ESBException("Dictionary of message cannot be published using this method");
            Message<TMessage>(message);
            Publish();
        }

        /// <summary>
        /// Delete topic if no subscriber exists for the topic.
        /// Will throw exception if topic have subscriber
        /// </summary>
        public void Delete() {
            ValidateState();
            ESB.Execute(messageBus => messageBus.DeleteTopic(_name));
            _publishDicts.Clear();
            _headers.Clear();
            _filter = null;
        }

        /// <summary>
        /// Throw an exception if the topic is in invalid state
        /// </summary>
        private void ValidateState() {
            if (!IsValid) throw new TopicNotRegisteredException(ExceptionConstants.TopicExceptionStr(_name));
        }

        /// <summary>
        /// Set the state of the current topic
        /// </summary>
        /// <param name="state"></param>
        /// <returns></returns>
        internal Topic Valid(bool value) {
            IsValid = value;
            return this;
        }

        /// <summary>
        /// Publish a collection of messages for the current topic
        /// </summary>
        /// <typeparam name="TMessage"></typeparam>
        /// <param name="messages"></param>
        public void PublishMany<TMessage>(ICollection<TMessage> messages) {
            ValidateState();
            _publishDicts.Clear();
            foreach (var message in messages) {
                var dict = message.ToDict();
                _publishDicts.Add(dict);
            }
            InternalPublish();
        }

        /// <summary>
        /// Publish the current topic
        /// </summary>
        internal void InternalPublish() {
            //Cannot batch sequential messages or message with expiration
            var allowBatch = (!Headers.ContainsKey(Constants.ESB_GROUP_ID_KEY) ||
                !ExpiresIn.HasValue) && ESB.AutoBatch;
            if (allowBatch) {
                TopicBatch.Publish(this);
                return;
            }

            ESB.Execute(messageBus => messageBus.PublishTopic(this.ToTopicInfo()));
            _publishDicts.Clear();
            _headers.Clear();
            _filter = null;
        }
       
        /// <summary>
        /// Publish the current topic
        /// </summary>
        public void Publish() {
            ValidateState();
            _publishDicts = new List<Dictionary<string, object>>() { _publishDict };
            InternalPublish();
        }
    }
    
    public static partial class TopicExtension {
        internal static Topic Clone(this Topic value) {
            var topic = Topic.New(value.TopicName)
                        .Valid(value.IsValid)
                        .Id(value.TopicID)
                        .Description(value.TopicDescription);
            topic.ContractDict = value.ContractDict;
            return topic;
        }
    }
}
